using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

namespace SpellCenter.Core
{
  /// <summary>
  /// A file pattern matches filenames.
  /// The directory is explicitly specified but may recursively match files 
  /// which are children of directories that are children if required.
  /// The following special characters are defined:
  /// <list type="unordered">
  /// <item>A * matches zero or more characters.</item>
  /// <item>A ? matches any one character.</item>
  /// </list>
  /// </summary>
  /// <remarks>
  /// The match pattern is similar but NOT the same as the Windows patterns, 
  /// also defined in <see cref="System.IO.Directory.GetFiles(string,string,SearchOption)"/>
  /// The windows pattern differs because it has different semantics based on 
  /// the length of the extension.
  /// FilePatterns have consistent semantics.
  /// </remarks>
  sealed public class FilePattern : IEquatable<FilePattern>
    {
        private string _directory;
        private string _pattern;
        private Regex _patternAsRegEx;
        private SearchOption _searchOption;

        public FilePattern(string directory, string pattern, SearchOption searchOption)
        {
            Directory = directory;
            Pattern = pattern;
            SearchOption = searchOption;
        }

        public FilePattern()
        {
        }

        public string Pattern
        {
            get { return _pattern; }
            set { 
              _pattern = value;
              _patternAsRegEx = MakeRegExFromPattern(_pattern);
          }
        }

      private static Regex MakeRegExFromPattern(string pattern)
      {
        const string StartOfString = "^";
        const string EndOfString = "$";

        //convert the pattern to a regular expression
        //... allow some of the special regular expression syntax 
        //    characters by escaping the pattern 
        string patternAsRegex = Regex.Escape(pattern);
        //... treat * as any number of characters
        patternAsRegex = patternAsRegex.Replace("\\*", ".*"); 
        //... treat ? as any single character
        patternAsRegex = patternAsRegex.Replace("\\?", ".");
        //... must match entire string
        return new Regex(StartOfString + patternAsRegex + EndOfString);
      }

      public SearchOption SearchOption
        {
            get { return _searchOption; }
            set { _searchOption = value; }
        }

        public string Directory
        {
            get { return _directory; }
            set { _directory = value; }
        }

        public IEnumerable<string> GetMatches()
        {
            VerifyDirectoryInitialized();

          string[] files = System.IO.Directory.GetFiles(
                              this._directory, 
                              this._pattern, 
                              this._searchOption);
          foreach (string fileName in files)
            {
            // our pattern match is more specific than Windows
            // i.e. sometimes Windows returns more files than our
            // semantics would allow
            // see Directory.GetFiles(string, string) for more info
            if(MatchesPattern(Path.GetFileName(fileName)))
                yield return fileName;
            }
        }

      private void VerifyDirectoryInitialized()
      {
        if (string.IsNullOrEmpty(this._directory))
        {
          throw new InvalidOperationException("Directory must be initialized to non-null, non-empty string.");
        }
      }

      /// <summary>
      /// True if the pattern would match on file
      /// </summary>
      /// <param name="file"></param>
      /// <returns></returns>
      public bool AppliesTo(string file)
      {
        VerifyDirectoryInitialized();

        string directory = Path.GetDirectoryName(file);
        
        if (Directory != directory
          && (SearchOption == SearchOption.TopDirectoryOnly 
              || !IsSubdirectoryOfDirectory(new DirectoryInfo(directory), 
                                            new DirectoryInfo(Directory))))
        {
          return false;
        }

        return MatchesPattern(Path.GetFileName(file));
      }

      private bool MatchesPattern(string name)
      {
        return _patternAsRegEx.IsMatch(name);
      }

      #region IsMoreSpecificThan method and supporting methods

        /// <summary>
        /// Tests whether the current object is more specific than pattern
        /// </summary>
        /// <param name="pattern"></param>
        /// <returns></returns>
        public bool IsMoreSpecificThan(FilePattern pattern)
        {
          VerifyDirectoryInitialized();
          pattern.VerifyDirectoryInitialized();
          // a file pattern is considered to be more specific if it
          // is a subdirectory of another or if it has the same
          // directory and the given pattern would match it
          //  but not vice versa
          // if it can apply either way, then ? is more specific than *
          //   *.a is more specific than *.a* (*.a* matches *.a but not vice-versa)
          //   a.b is more specific than a.?  (a.? matches a.b but not vice-versa)
          //   ?.a is more specific than *.a  (*.a matches ?.a and vice-versa but ? is more specific than *)
          //   ?.* is not more specific than *.? (?.* matches *.? and vice-versa but have same number of ? and *)
          if (Directory == pattern.Directory)
          {
            // does the given pattern match this
            if (pattern.MatchesPattern(Pattern))
            {
              //does this match the given pattern
              if (MatchesPattern(pattern.Pattern))
              {
                // it can apply either way
                if (CountOfQuestionMarks(Pattern) >
                   CountOfQuestionMarks(pattern.Pattern))
                {
                  return true;
                }
              }
              else
              {
                return true;
              }
            }
          }
          else if (IsSubdirectoryOfDirectory(new DirectoryInfo(Directory),
                                        new DirectoryInfo(pattern.Directory)))
          {
              return true;
          }
          return false;
        }

      static private int CountOfQuestionMarks(string pattern)
      {
        int count = 0;
        int questionMarkIndex = pattern.IndexOf('?');
        while(questionMarkIndex != -1)
        {
          ++count;
          questionMarkIndex = pattern.IndexOf('?', questionMarkIndex+1);
        }
        return count;
      }

      static private bool IsSubdirectoryOfDirectory(DirectoryInfo subDirectory, FileSystemInfo baseDirectory)
      {
        // Equals and == on the DirectoryInfo doesn't seem to work correctly
        if (subDirectory.FullName == baseDirectory.FullName)
        {
          return false;
        }

        DirectoryInfo parent;
        parent = subDirectory.Parent;
        while(parent!=null)
        {
          if (parent.FullName == baseDirectory.FullName)
          {
            return true;
          }
          parent = parent.Parent;
        }
        return false;
      }

        #endregion

        #region Equality operators and GetHashCode()

        public static bool operator !=(FilePattern filePattern1, FilePattern filePattern2)
        {
            return !Equals(filePattern1, filePattern2);
        }

        public static bool operator ==(FilePattern filePattern1, FilePattern filePattern2)
        {
            return Equals(filePattern1, filePattern2);
        }

        public bool Equals(FilePattern filePattern)
        {
            if (filePattern == null)
            {
                return false;
            }
            if (!Equals(_directory, filePattern._directory))
            {
                return false;
            }
            if (!Equals(_pattern, filePattern._pattern))
            {
                return false;
            }
            if (!Equals(_searchOption, filePattern._searchOption))
            {
                return false;
            }
            return true;
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(this, obj))
            {
                return true;
            }
            return Equals(obj as FilePattern);
        }

        public override int GetHashCode()
        {
          int result = _directory != null ? _directory.GetHashCode() : 0;
            result = 29 * result + (_pattern != null ? _pattern.GetHashCode() : 0);
            result = 29 * result + _searchOption.GetHashCode();
            return result;
        }

        #endregion

        public override string ToString()
        {
            return string.Format("FilePattern<{0}, {1}, {2}>", _pattern, _directory, _searchOption);
        }
    }
}